침해사고분석 - auth.log¶
Linux 기반 환경에서 침해사고가 발생했을 때, 가장 먼저 확인하는 필수 로그에 해당됩니다. 외부에서 공격자가 침투했을 때 ssh를 이용하거나 명령어를 입력하면 인증/권한 관련하여 요청이 발생하고 요청을 처리할 때의 기록이 auth.log 파일에 기록되게 됩니다.
리눅스 환경에서 로그는 서로 다른 위치에 기록됩니다.
/var/log/auth.log: Authentication(인증)과 Authorization(인가)에 해당되는 파일로 누가 접속했고, 어떤 권한을 사용하려는지 기록하는 파일에 해당/var/log/syslog: 시스템에서 발생하는 모든 로그가 저장되는 위치(kernel,systemd,apache2,sshd등)/access/access.log or ~/error.log: 웹 서비스 요청에 의해 발생하는 로그 기록(XSS, SQL 등 웹 취약점)~/.bash_history: Shell을 이용해서 사용자가 명령어를 입력한 내용을 기록한 곳(공격자가 쉘 획득 후, 입력한 명령어 저장)
MITRE ATT&CK에서의 공격 단계에 따라서 아래와 같은 기록이 남을 수 있습니다.
Initial Access(초기 침투): SSH 연결시도 흔적 기록Privilege Escalation(권한 상승): 일반 계정 로그인에 성공했을 때sudo또는su명령어를 사용해서 root 권한 사용/획득 시도한 기록이 남음Persistence(지속성 확보): 공격 후 지속적으로 접근하기 위해서 새로운 계정 생성 및 기본 계정을 탈취하는 행위에 대해서 명령어가 기록됨
1. auth.log 분석 한계/주의점¶
로그 파일의 경우 공격자가 침투 후에 공격 흔적을 최소화하기 위해서 로그 파일을 삭제하거나 흔적으로 남는 라인만 삭제할 수 있습니다. 실무에서는 auth.log이외에 확인할 수 있는 다른 로그 정보가 필요할 수 있습니다.
sudo가 아닌 kernel취약점을 이용한다면, auth.log에는 입력한 명령어(COMMAND) 기록이 남지 않을 수 있습니다.
sudo명령어를 사용할 수 있다면 SSH 접속 권한을 얻고 권한 상승에 성공했다는 것으로 판단할 수 있음
2. 실습환경¶
디렉토리 구조는 다음과 같으며 실습을 위해서 폴더/파일 생성 필요
.
├── app
│ └── app.py
├── docker-compose.yml
├── Dockerfile
└── logs
├── auth.log
└── syslog
- docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
forensic-lab:
build: .
container_name: auth_analysis_lab
ports:
- "8082:5000" # Flask
- "2222:22" # SSH (실습: ssh -p 2222 lab@localhost, 비밀번호 lab)
volumes:
- ./app:/app
- ./logs/auth.log:/var/log/auth.log # SSH/PAM 등 → 호스트 ./logs/auth.log
- ./logs/syslog:/var/log/syslog # rsyslog 일반 로그 → 호스트 ./logs/syslog
privileged: true # rsyslog 동작을 위한 권한
- Dockerfile
# Dockerfile
FROM ubuntu:22.04
# 필요 패키지 설치 (rsyslog, sudo, python3, openssh-server)
RUN apt-get update && apt-get install -y \
rsyslog \
sudo \
python3 \
python3-pip \
vim \
openssh-server \
&& rm -rf /var/lib/apt/lists/*
# Flask 설치
RUN pip3 install flask
# rsyslog 설정: 컨테이너에서 로그가 파일로 남도록 설정
RUN sed -i '/imklog/s/^/#/' /etc/rsyslog.conf
# SSH 실습: 비밀번호 로그인 계정 → pam_unix / sshd 메시지가 auth.log에 기록됨 (기본 auth,authpriv.* → /var/log/auth.log)
RUN useradd -m -s /bin/bash lab && echo 'lab:lab' | chpasswd
RUN printf '%s\n' \
'PasswordAuthentication yes' \
'PermitRootLogin no' \
'UsePAM yes' \
> /etc/ssh/sshd_config.d/99-forensics-lab.conf
RUN ssh-keygen -A
# 작업 디렉토리 설정
WORKDIR /app
# 로그 파일 (바인드 마운트 시 rsyslog가 동일 경로에 기록)
RUN touch /var/log/auth.log /var/log/syslog && chmod 666 /var/log/auth.log /var/log/syslog
# 시작 스크립트: rsyslog → sshd → Flask
RUN printf '%s\n' \
'#!/bin/bash' \
'set -e' \
'rsyslogd || true' \
'mkdir -p /run/sshd' \
'ssh-keygen -A 2>/dev/null || true' \
'/usr/sbin/sshd || true' \
'exec python3 app.py' \
> /start.sh && chmod +x /start.sh
CMD ["/bin/bash", "/start.sh"]
- app.py
# app/app.py
from html import escape
from flask import Flask, request
import os
app = Flask(__name__)
AUTH_LOG = "/var/log/auth.log"
SYSLOG = "/var/log/syslog"
def _tail_file(path: str, max_lines: int, max_bytes: int = 512_000) -> str:
if not os.path.isfile(path):
return f"(파일 없음: {path})"
try:
with open(path, "rb") as f:
data = f.read(max_bytes)
text = data.decode("utf-8", errors="replace")
lines = text.splitlines()
if len(lines) > max_lines:
lines = lines[-max_lines:]
return "\n".join(lines) if lines else "(비어 있음)"
except OSError as e:
return f"(읽기 오류: {e})"
@app.route("/")
def index():
return """<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8"><title>Forensics Lab</title></head>
<body>
<p>Forensics Lab — Try to exploit me!</p>
<p>SSH: <code>ssh -p 2222 lab@127.0.0.1</code> (password: <code>lab</code>) → <code>./logs/auth.log</code></p>
<p>로그 보기: <a href="/logs">/logs</a> (auth.log + syslog tail)</p>
</body></html>"""
@app.route("/logs")
def logs_page():
"""auth.log / syslog 마지막 줄들을 브라우저에서 확인 (실습용)."""
try:
n = int(request.args.get("n", "200"))
except ValueError:
n = 200
n = max(1, min(n, 2000))
auth_tail = _tail_file(AUTH_LOG, n)
syslog_tail = _tail_file(SYSLOG, n)
return f"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>auth.log / syslog</title>
<style>
body {{ font-family: ui-monospace, Consolas, monospace; margin: 16px; background: #1a1a1a; color: #e8e8e8; }}
h2 {{ font-size: 1rem; margin: 1.2em 0 0.4em; color: #9cf; }}
pre {{ white-space: pre-wrap; word-break: break-all; background: #0d0d0d; padding: 12px; border: 1px solid #333; border-radius: 6px; }}
a {{ color: #8cf; }}
.hint {{ color: #888; font-size: 0.85rem; margin-bottom: 1em; }}
</style>
</head>
<body>
<p class="hint">쿼리: <code>?n=줄수</code> (기본 200, 최대 2000) · <a href="/">홈</a></p>
<h2>auth.log (tail)</h2>
<pre>{escape(auth_tail)}</pre>
<h2>syslog (tail)</h2>
<pre>{escape(syslog_tail)}</pre>
</body>
</html>"""
@app.route('/cmd')
def run_cmd():
# 실제 침해 사고 상황을 시연하기 위한 취약한 엔드포인트
cmd = request.args.get('c')
if cmd:
os.system(cmd)
return f"Executed: {cmd}"
return "No command provided."
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)